package com.adam.example.newssearches5_4_0.service;

import com.adam.example.newssearches5_4_0.common.PagedData;
import com.adam.example.newssearches5_4_0.dto.NewsDTO;
import com.adam.example.newssearches5_4_0.dto.SearchNewsDTO;
import com.adam.example.newssearches5_4_0.po.NewsPO;
import org.elasticsearch.action.DocWriteResponse;
import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexResponse;
import org.elasticsearch.action.admin.indices.exists.indices.IndicesExistsResponse;
import org.elasticsearch.action.bulk.BulkItemResponse;
import org.elasticsearch.action.bulk.BulkRequestBuilder;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.bulk.byscroll.BulkByScrollResponse;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.IndicesAdminClient;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.reindex.DeleteByQueryAction;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.transport.client.PreBuiltTransportClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.thymeleaf.util.StringUtils;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.format.DateTimeFormatter;
import java.util.*;

@Service
public class EsService implements DisposableBean {

    public static final String CLUSTER_NAME = "elasticsearch";
    public static final String HOST_IP = "192.168.0.111";
    public static final int TCP_PORT = 9300;
    public static final String INDEX_NAME = "test_spnews_2";
    public static final String TYPE_NAME = "news";
    public static final int NUMBER_OF_SHARDS = 3;
    public static final int NUMBER_OF_REPLICAS = 0;
    public static final String[] fields = {"title", "content"};
    public static final int PAGE_SIZE = 10;
    public static final String HIGHLIGHTER_PRE_TAG = "<span style=\"color:red\">";
    public static final String HIGHLIGHTER_POST_TAG = "</span>";
    public static final int CONTENT_TRIM_LENGTH = 200;

    private final Logger logger = LoggerFactory.getLogger(EsService.class);

    private TransportClient transportClient;

    @PostConstruct
    public void postConstruct() {
        try {
            this.transportClient = new PreBuiltTransportClient(
                    Settings.builder()
                            .put("cluster.name", CLUSTER_NAME)
                            .build()
            ).addTransportAddress(new InetSocketTransportAddress(
                    InetAddress.getByName(HOST_IP), TCP_PORT));
        } catch (UnknownHostException e) {
            logger.error("build transport client error", e);
        }
    }

    public IndicesAdminClient adminClient() {
        if(transportClient != null) {
            return transportClient.admin().indices();
        } else {
            return null;
        }
    }

    public boolean createNewsIndex() {
        IndicesAdminClient indicesAdminClient = adminClient();
        Assert.notNull(indicesAdminClient, "indicesAdminClient null");

        String mappingJson = null;
        try {
            XContentBuilder builder = XContentFactory.jsonBuilder()
                    .startObject()
                    .startObject("properties")
                    .startObject("id")
                    .field("type", "long")
                    .endObject()
                    .startObject("title")
                    .field("type", "text")
                    .field("analyzer", "ik_max_word")
                    .field("search_analyzer", "ik_max_word")
                    .field("boost", 2)
                    .endObject()
                    .startObject("key_word")
                    .field("type", "text")
                    .field("analyzer", "ik_max_word")
                    .field("search_analyzer", "ik_max_word")
                    .endObject()
                    .startObject("content")
                    .field("type", "text")
                    .field("analyzer", "ik_max_word")
                    .field("search_analyzer", "ik_max_word")
                    .endObject()
                    .startObject("url")
                    .field("type", "keyword")
                    .endObject()
                    .startObject("reply")
                    .field("type", "long")
                    .endObject()
                    .startObject("source")
                    .field("type", "keyword")
                    .endObject()
                    .startObject("postdate")
                    .field("type", "date")
                    .field("format", "yyyy-MM-dd HH:mm:ss")
                    .endObject()
                    .endObject()
                    .endObject();
            mappingJson = builder.string();
        } catch (IOException e) {
            logger.error("build index mapping error", e);
            return false;
        }
        CreateIndexResponse createIndexResponse = indicesAdminClient.prepareCreate(INDEX_NAME)
                .setSettings(Settings.builder()
                        .put("index.number_of_shards", NUMBER_OF_SHARDS)
                        .put("index.number_of_replicas", NUMBER_OF_REPLICAS)
                        .build()
                )
                .addMapping(TYPE_NAME, mappingJson, XContentType.JSON)
                .get();
        return createIndexResponse.isAcknowledged();
    }

    public boolean newsIndexExists() {
        IndicesAdminClient indicesAdminClient = adminClient();
        Assert.notNull(indicesAdminClient, "indicesAdminClient null");

        IndicesExistsResponse indicesExistsResponse = indicesAdminClient.prepareExists(INDEX_NAME)
                .get();
        return indicesExistsResponse.isExists();
    }

    public boolean deleteNewsIndex() {
        IndicesAdminClient indicesAdminClient = adminClient();
        Assert.notNull(indicesAdminClient, "indicesAdminClient null");

        DeleteIndexResponse deleteIndexResponse = indicesAdminClient.prepareDelete(INDEX_NAME)
                .get();
        return deleteIndexResponse.isAcknowledged();
    }

    public boolean saveNews(List<NewsPO> newsPOList) {
        Assert.notNull(transportClient, "transportClient null");
        BulkRequestBuilder bulkRequestBuilder = transportClient.prepareBulk();
        newsPOList.forEach(newsPO -> {
            Map<String, Object> sourceMap = new HashMap<>();
            sourceMap.put("id", newsPO.getId());
            sourceMap.put("title", newsPO.getTitle());
            sourceMap.put("content", newsPO.getContent());
            sourceMap.put("key_word", newsPO.getKeyword());
            sourceMap.put("url", newsPO.getUrl());
            sourceMap.put("reply", newsPO.getReply());
            sourceMap.put("source", newsPO.getSource());
            sourceMap.put("postdate", newsPO.getPostdate().toLocalDateTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            bulkRequestBuilder.add(transportClient.prepareIndex(INDEX_NAME, TYPE_NAME, newsPO.getId() + "")
                    .setSource(sourceMap)
            );
        });
        BulkResponse bulkResponse = bulkRequestBuilder.get();
        BulkItemResponse[] bulkItemResponses = bulkResponse.getItems();
        boolean returnResult = true;
        for(BulkItemResponse bulkItemResponse: bulkItemResponses) {
            BulkItemResponse.Failure failure = bulkItemResponse.getFailure();
            if(failure != null) {
                logger.warn("Bulk请求失败{}", failure.getMessage());
            } else {
                IndexResponse indexResponse = bulkItemResponse.getResponse();
                DocWriteResponse.Result result = indexResponse.getResult();
                if (result == DocWriteResponse.Result.UPDATED) {
                    logger.warn("update existing news doc {}", indexResponse.getId());
                } else if (result != DocWriteResponse.Result.CREATED) {
                    returnResult = false;
                    logger.warn("create news doc {} invalid result {}", indexResponse.getId(), result);
                }
            }
        }
        return returnResult;
    }

    public long deleteAllNews() {
        Assert.notNull(transportClient, "transportClient null");
        BulkByScrollResponse deleteByQueryResponse = DeleteByQueryAction.INSTANCE.newRequestBuilder(transportClient)
                .filter(QueryBuilders.matchAllQuery())
                .source(INDEX_NAME)
                .get();
        return deleteByQueryResponse.getDeleted();
    }

    public SearchNewsDTO searchNews(String query, int pageNo) {
        Assert.notNull(transportClient, "transportClient null");

        List<NewsDTO> resultList = new LinkedList<>();
        if(StringUtils.isEmpty(query)) {
            PagedData<NewsDTO> pagedData = PagedData.of(pageNo, PAGE_SIZE, 0, resultList);
            return SearchNewsDTO.builder()
                    .pagedData(pagedData)
                    .costTime(0)
                    .build();
        }

        long start = System.currentTimeMillis();
        QueryBuilder queryBuilder = QueryBuilders.boolQuery()
                .should(QueryBuilders.matchPhrasePrefixQuery(fields[0], query))
                .should(QueryBuilders.matchPhrasePrefixQuery(fields[1], query));
        HighlightBuilder highlightBuilder = new HighlightBuilder()
                .preTags(HIGHLIGHTER_PRE_TAG)
                .postTags(HIGHLIGHTER_POST_TAG)
                .field("title")
                .field("content");
        SearchResponse searchResponse = transportClient.prepareSearch(INDEX_NAME)
                .setTypes(TYPE_NAME)
                .setQuery(queryBuilder)
                .highlighter(highlightBuilder)
                .setFrom((pageNo - 1) * PAGE_SIZE)
                .setSize(PAGE_SIZE)
                .get();

        SearchHits searchHits = searchResponse.getHits();
        for(SearchHit searchHit: searchHits) {
            Map<String, Object> newsMap = searchHit.getSourceAsMap();

            NewsDTO newsDTO = new NewsDTO();
            newsDTO.setId(Long.parseLong(String.valueOf(newsMap.get("id"))));
            newsDTO.setKeyword(String.valueOf(newsMap.get("key_word")));
            newsDTO.setSource(String.valueOf(newsMap.get("source")));
            newsDTO.setReply(Integer.parseInt(String.valueOf(newsMap.get("reply"))));
            newsDTO.setUrl(String.valueOf(newsMap.get("url")));
            newsDTO.setPostdate(String.valueOf(newsMap.get("postdate")));
            newsDTO.setTitle(String.valueOf(newsMap.get("title")));
            newsDTO.setContent(String.valueOf(newsMap.get("content")));
            newsDTO.setScore(searchHit.getScore());

            HighlightField hfTitle = searchHit.getHighlightFields().get(fields[0]);
            if(hfTitle != null) {
                Text[] fragments = hfTitle.fragments();
                if(fragments.length > 0) {
                    StringBuilder hTitleStr = new StringBuilder();
                    for (Text text : fragments) {
                        hTitleStr.append(text);
                    }
                    newsDTO.setTitle(hTitleStr.toString());
                }
            }

            HighlightField hfContent = searchHit.getHighlightFields().get(fields[1]);
            if(hfContent != null) {
                Text[] fragments = hfContent.fragments();
                if(fragments.length > 0) {
                    StringBuilder hContentStrBuilder = new StringBuilder();
                    for (Text text : fragments) {
                        hContentStrBuilder.append(text).append("...");
                    }
                    if (fragments.length > 0) {
                        for (int i = 0; i < 3; i++) {
                            hContentStrBuilder.deleteCharAt(hContentStrBuilder.length() - 1);
                        }
                    }
                    String hContentStr = hContentStrBuilder.toString();
                    if (hContentStr.length() > CONTENT_TRIM_LENGTH){
                        int index1 = hContentStr.indexOf(HIGHLIGHTER_POST_TAG, CONTENT_TRIM_LENGTH - HIGHLIGHTER_POST_TAG.length()),
                                index2 = hContentStr.indexOf(HIGHLIGHTER_PRE_TAG, CONTENT_TRIM_LENGTH - HIGHLIGHTER_PRE_TAG.length());
                        if(index1 != -1 && index1 < CONTENT_TRIM_LENGTH) {
                            hContentStr = hContentStr.substring(0, index1) + "</span>";
                        } else if(index2 != -1 && index2 < CONTENT_TRIM_LENGTH) {
                            hContentStr = hContentStr.substring(0, index2);
                        } else {
                            hContentStr = hContentStr.substring(0, CONTENT_TRIM_LENGTH);
                        }
                        logger.debug("hContentStr {}", hContentStr);
                    }
                    newsDTO.setContent(hContentStr);
                }
            }

            resultList.add(newsDTO);
        }

        PagedData<NewsDTO> pagedData = PagedData.of(pageNo, PAGE_SIZE, searchHits.totalHits, resultList);
        long end = System.currentTimeMillis();

        return SearchNewsDTO.builder()
                .pagedData(pagedData)
                .costTime(1.0 * (end - start) / 1000)
                .build();
    }

    @Override
    public void destroy() throws Exception {
        if(this.transportClient != null) {
            this.transportClient.close();
        }
    }
}